package jmine.tec.utils.collection;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Classes utilitarias
 * 
 * @author takeshi
 */
public final class MapperUtils {

    /**
     * C'tor privado
     */
    private MapperUtils() {
    }

    /**
     * Mapper de identidade - devolve o input
     * 
     * @author takeshi
     */
    private static final class IdentityMapper implements Mapper, Serializable {

        /**
         * {@inheritDoc}
         */
        public Object apply(Object input) {
            return input;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString() {
            return "Identity";
        }

        /**
         * Faz com que, ao ser deserializado, seja devolvido o mesmo objeto
         * 
         * @return Object
         */
        protected Object readResolve() {
            return IDENTITY_MAPPER;
        }

    }

    public static final Mapper IDENTITY_MAPPER = new IdentityMapper();

    /**
     * Devolve um mapper identidade
     * 
     * @param <T> tipo
     * @return Mapper
     */
    @SuppressWarnings("unchecked")
    public static <T> Mapper<T, T> getIdentityMapper() {
        return IDENTITY_MAPPER;
    }

    /**
     * Cria um chain mapper
     * 
     * @param <A> o tipo de entrada
     * @param <B> o tipo do 'meio'
     * @param <C> o tipo de saida
     * @param a o mapper 'interno'
     * @param b o mapper 'externo'
     * @return Mapper de A para C
     */
    public static <A, B, C> Mapper<A, C> chainMapper(final Mapper<A, B> a, final Mapper<B, C> b) {
        return new ChainingMapper<A, C, B>(a, b);
    }

    /**
     * Aplica um Mapper em uma lista, devolvendo uma nova lista com os elementos resultantes, mantendo a ordem dos elementos
     * 
     * @param <A> o tipo da lista original
     * @param <B> o tipo do resultado
     * @param originalList a lista original
     * @param mapper o Mapper
     * @return List
     */
    public static <A, B> List<B> mapList(Collection<? extends A> originalList, Mapper<? super A, ? extends B> mapper) {
        List<B> list = new ArrayList<B>(originalList.size());
        for (A a : originalList) {
            B b = mapper.apply(a);
            list.add(b);
        }
        return list;
    }

    /**
     * Aplica um {@link Mapper} em um {@link Set}, devolvendo um novo conjunto com os elementos resultantes mantendo a ordem dos elementos.
     * O conjunto resultante, porem, pode ter elementos a menos se o {@link Mapper} nao for injetora
     * 
     * @param <A> o tipo do set original
     * @param <B> o tipo do set resultado
     * @param originalSet o set original
     * @param mapper o {@link Mapper}
     * @return {@link Set}
     */
    public static <A, B> Set<B> mapSet(Collection<? extends A> originalSet, Mapper<? super A, ? extends B> mapper) {
        Set<B> set = new LinkedHashSet<B>(originalSet.size());
        for (A a : originalSet) {
            B b = mapper.apply(a);
            set.add(b);
        }
        return set;
    }

    /**
     * Mapea os valores do mapa passado, devolvendo um novo mapa com os resultados, mantendo a ordem do mapa original bem como as
     * associacoes chave -> valor
     * 
     * @param <K> tipo da chave do mapa
     * @param <A> tipo do valor original
     * @param <B> tipo do valor do mapa resultado
     * @param originalMap mapa original
     * @param mapper {@link Mapper}
     * @return {@link Map}
     */
    public static <K, A, B> Map<K, B> mapMapValues(Map<K, A> originalMap, Mapper<? super A, ? extends B> mapper) {
        Map<K, B> map = new LinkedHashMap<K, B>(originalMap.size());
        for (Entry<K, A> entry : originalMap.entrySet()) {
            B b = mapper.apply(entry.getValue());
            map.put(entry.getKey(), b);
        }
        return map;
    }

    /**
     * Mapea as chaves do mapa passado, devolvendo um novo mapa com os resultados. O mapa resultado pode ter resultados a menos se a funcao
     * nao for injetora.
     * 
     * @param <V> tipo do valor do mapa
     * @param <A> tipo do valor original
     * @param <B> tipo do valor do mapa resultado
     * @param originalMap mapa original
     * @param mapper {@link Mapper}
     * @return {@link Map}
     */
    public static <V, A, B> Map<B, V> mapMapKeys(Map<A, V> originalMap, Mapper<? super A, ? extends B> mapper) {
        Map<B, V> map = new LinkedHashMap<B, V>(originalMap.size());
        for (Entry<A, V> entry : originalMap.entrySet()) {
            B b = mapper.apply(entry.getKey());
            map.put(b, entry.getValue());
        }
        return map;
    }

}
